#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/stat.h>
#if defined(__APPLE__) && defined(__MACH__)
#include <TargetConditionals.h>
#endif
#include <ev.h>
#include "config.h"
#include "log.h"
#include "filelock.h"
#include "io.h"
#include "client.h"
#include "crypt.h"
#include "resolv.h"


static struct cfg cfg = {
        .local_bind = DEF_BIND_ADDR,
        .local_listen = DEF_LISTEN_PORT,
        .socket_timeout = DEF_SOCKET_TIMEOUT,
        .connect_timeout = DEF_CONNECT_TIMEOUT,
        .accept_timeout = DEF_ACCEPT_TIMEOUT,
        .dns_cache_expire = DEF_CACHE_EXPIRE,
        .server_initial_weight = DEF_SERVER_WEIGHT, // FIXME: add config for max weight
};

static void daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct sigaction sa;

    /* Clear file creation mask. */
    umask(0);

    /* Become a session leader to lose controlling TTY. */
    if ((pid = fork()) < 0) {
        fprintf(stderr, "%s: can't fork: %s\n", cmd, strerror(errno));
        exit(EXIT_FAILURE);
    } else if (pid != 0) /* parent */
        exit(EXIT_SUCCESS);
    setsid();

    /* Ensure future opens won't allocate controlling TTYs. */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        fprintf(stderr, "%s: can't ignore SIGHUP\n", cmd);
        exit(EXIT_FAILURE);
    }
    if ((pid = fork()) < 0) {
        fprintf(stderr, "%s: can't fork: %s\n", cmd, strerror(errno));
        exit(EXIT_FAILURE);
    } else if (pid != 0) /* parent */
        exit(EXIT_SUCCESS);

    /* Change the current working directory to the root so
     * we won't prevent file systems from being unmounted. */
    if (chdir("/") < 0) {
        fprintf(stderr, "%s: can't change directory to /\n", cmd);
        exit(EXIT_FAILURE);
    }

    /* Close all open file descriptors. */
    for (i = 0; (unsigned int)i < cfg.open_max; i++)
        close(i);

    /* Attach file descriptors 0, 1, and 2 to /dev/null. */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    /* Initialize the log file. */
    OPENLOG(cmd, FXS_LOG_OPTION, LOG_USER);
    SETLOGMASK(FXS_LOG_MASK);
    LOGN("is startup");

    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        LOGC("unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(EXIT_FAILURE);
    }
}

static int already_running(const char *pidfile, const char *ident, int *lckfd)
{
    int fd;
    char buf[16];
    char filename[256];
    const char *lockfilename;

    if (!pidfile) {
        snprintf(filename, sizeof(filename), DEF_PID_FILE, ident);
        lockfilename = filename;
    } else {
        lockfilename = pidfile;
    }

    if ((fd = open(lockfilename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) < 0) {
        LOGC("can't open lock file %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (write_lock(fd, 0, SEEK_SET, 0) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return 1;
        }
        LOGC("can't lock %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (ftruncate(fd, 0) < 0) {
        LOGC("can't truncate lock file %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }
    snprintf(buf, sizeof(buf), "%ld", (long)getpid());
    if (safe_write(fd, buf, strlen(buf) + 1) < 0) {
        LOGC("can't write pid to %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (lckfd)
        *lckfd = fd;

    return 0;
}

static void sigCleanupHandler(int signo) {
    switch (signo) {
    case SIGINT:
        LOGN("Got SIGINT, exiting...");
        break;
    case SIGTERM:
        LOGN("Got SIGTERM, exiting...");
        break;
    case SIGQUIT:
        LOGN("Got SIGQUIT, exiting...");
        break;
    default:
        return;
    }

    exit(EXIT_SUCCESS);
}

static int sigactions(void) {
    struct sigaction sa;

    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sa.sa_flags = 0;
    if (sigaction(SIGINT, &sa, NULL) < 0) {
        LOGE("Can't catch SIGINT: %s", strerror(errno));
        return -1;
    }
    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        LOGE("Can't catch SIGTERM: %s", strerror(errno));
        return -1;
    }
    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGQUIT, &sa, NULL) < 0) {
        LOGE("Can't catch SIGQUIT: %s", strerror(errno));
        return -1;
    }

    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGPIPE, &sa, NULL) < 0) {
        LOGE("Can't ignore SIGPIPE: %s", strerror(errno));
        return -1;
    }

    return 0;
}

int main(int argc, char *argv[]) {
    struct resolv_ctx *rctx;
    struct client_ctx *cctx;
    struct ev_loop *loop;

    config(&cfg, argc, argv);

    if (cfg.daemon) {
        daemonize(cfg.prog_name);
    } else {
        OPENLOG(cfg.prog_name, FXS_LOG_OPTION | LOG_PERROR, LOG_USER);
        SETLOGMASK(LOG_UPTO(LOG_DEBUG));
        LOGN("is startup");
    }

    if (already_running(cfg.pidfile, cfg.prog_name, NULL)) {
        LOGC("already running");
        exit(EXIT_FAILURE);
    }

    sigactions();

    crypt_env_init();

    loop = EV_DEFAULT;

    if (!(rctx = resolv_ctx_create(cfg.dns_cache_expire, loop)))
        exit(EXIT_FAILURE);

    if (!(cctx = client_ctx_create(&cfg, rctx, loop)))
        exit(EXIT_FAILURE);

    LOGD("open max: %llu", cfg.open_max);
    LOGD("buffer size: %"FMT_OF_SIZE, cfg.buf_size);

    ev_run(loop, 0);

    return EXIT_SUCCESS;
}
